#include "mkvmuxfilter.hpp"
#include "mkvstream.hpp"
#include <vfwmsgs.h>
#include <cassert>
#ifdef _DEBUG
#include "odbgstream.hpp"
using std::endl;
using std::hex;
using std::dec;
#endif

namespace MkvMux
{


Inpin::Lock::Lock(Inpin* inpin) :
    m_pFilter(inpin->m_pFilter)
{
    m_pFilter->EnterCriticalSection();
}


Inpin::Lock::~Lock()
{
    if (m_pFilter)
        m_pFilter->LeaveCriticalSection();
}


void Inpin::Lock::Seize(Inpin* inpin)
{
    assert(inpin);
    assert(m_pFilter == 0);
    
    m_pFilter = inpin->m_pFilter;
    m_pFilter->EnterCriticalSection();
}


void Inpin::Lock::Release()
{
    Filter* const pFilter = m_pFilter;
    assert(pFilter);
    
    m_pFilter = 0;

    pFilter->LeaveCriticalSection();
}


Inpin::Inpin(Filter* p, const wchar_t* id) : 
    Pin(p, id, PINDIR_INPUT),
    m_pStream(0)
{
    m_hSample = CreateEvent(0, 0, 0, 0);
    assert(m_hSample);  //TODO
    
    m_hStateChangeOrFlush = CreateEvent(0, 0, 0, 0);
    assert(m_hStateChangeOrFlush);  //TODO
}


Inpin::~Inpin()
{
    BOOL b = CloseHandle(m_hStateChangeOrFlush);
    assert(b);
    
    b = CloseHandle(m_hSample);
    assert(b);
}


void Inpin::Init()
{
   m_bFlush = false;
   m_bEndOfStream = false;
   
   BOOL b = ResetEvent(m_hSample);
   assert(b);
   
   b = ResetEvent(m_hStateChangeOrFlush);
   assert(b);

   assert(m_pStream == 0);

   if (m_connection == 0)
      return;

   const HRESULT hr = OnInit();
   hr;
   assert(SUCCEEDED(hr));
   assert(m_pStream);
}


void Inpin::Final()
{
    OnFinal();

    delete m_pStream;
    m_pStream = 0;
   
    const BOOL b = SetEvent(m_hStateChangeOrFlush);  //transition to stopped
    assert(b);
}


void Inpin::Run()
{
    const BOOL b = SetEvent(m_hStateChangeOrFlush);
    assert(b);
}


HRESULT Inpin::QueryInterface(const IID& iid, void** ppv)
{
    if (ppv == 0)
        return E_POINTER;
        
    IUnknown*& pUnk = reinterpret_cast<IUnknown*&>(*ppv);
    
    if (iid == __uuidof(IUnknown))
        pUnk = static_cast<IPin*>(this);
        
    else if (iid == __uuidof(IPin))
        pUnk = static_cast<IPin*>(this);
        
    else if (iid == __uuidof(IMemInputPin))
        pUnk = static_cast<IMemInputPin*>(this);
        
    else
    {
        pUnk = 0;
        return E_NOINTERFACE;
    }
    
    pUnk->AddRef();
    return S_OK;
}
    

ULONG Inpin::AddRef()
{
    return m_pFilter->AddRef();
}


ULONG Inpin::Release()
{
    return m_pFilter->Release();
}


HRESULT Inpin::Connect(IPin*, const AM_MEDIA_TYPE*)
{
    return E_UNEXPECTED;  //for output pins only
}


HRESULT Inpin::QueryInternalConnections( 
    IPin** pa,
    ULONG* pn)
{
    if (pn == 0)
        return E_POINTER;
        
    Lock lock(this);
    
    if (*pn == 0)
    {
        if (pa == 0)  //query for required number
        {
            *pn = 1;
            return S_OK;
        }
        
        return S_FALSE;  //means "insufficient number of array elements"
    }
    
    if (pa == 0)
    {
        *pn = 0;
        return E_POINTER;
    }
    
    IPin*& pin = pa[0];
    
    pin = &m_pFilter->m_outpin;
    pin->AddRef();
    
    *pn = 1;
    return S_OK;        
}


HRESULT Inpin::ReceiveConnection( 
    IPin* pin,
    const AM_MEDIA_TYPE* pmt)
{
    if ((pin == 0) || (pmt == 0))
        return E_POINTER;
        
    Lock lock(this);
        
    if (m_pFilter->m_state != State_Stopped)
        return VFW_E_NOT_STOPPED;

    if (m_connection)
        return VFW_E_ALREADY_CONNECTED;

    //assert(m_connection_mt.Empty());
    m_connection_mt.Clear();

    HRESULT hr = QueryAccept(pmt);
    
    if (hr != S_OK)
        return VFW_E_TYPE_NOT_ACCEPTED;
        
    const AM_MEDIA_TYPE& mt = *pmt;
        
    hr = OnReceiveConnection(pin, mt);  //dispatch to subclass
    
    if (FAILED(hr))
        return hr;

    hr = m_connection_mt.Add(mt);
    
    if (FAILED(hr))
        return hr;
                    
    m_connection = pin;
    m_connection->AddRef();
    
    return S_OK;
}


HRESULT Inpin::EndOfStream()
{
    Lock lock(this);
        
    if (m_connection == 0)
        return VFW_E_NOT_CONNECTED;
        
    if (m_pFilter->m_state == State_Stopped)
        return VFW_E_WRONG_STATE;  //?

    if (m_bEndOfStream)
        return S_FALSE;

    m_bEndOfStream = true;

    return OnEndOfStream();
}
    

HRESULT Inpin::BeginFlush()
{
    Lock lock(this);
        
    if (m_connection == 0)
        return VFW_E_NOT_CONNECTED;
        
    //TODO: check state?
    
    //I think flush is supposed to flush EOS indicators
    //too (they can be queued, just like sammples).
    
    if (m_bFlush)
        return S_FALSE;
        
    if (m_pStream)
        m_pStream->Flush();
    //TODO: destroy stream, then re-create it during EndFlush
        
    m_bFlush = true;
    
    const BOOL b = SetEvent(m_hStateChangeOrFlush);
    assert(b);
    
    return S_OK;
}
    

HRESULT Inpin::EndFlush()
{
    Lock lock(this);
        
    if (m_connection == 0)
        return VFW_E_NOT_CONNECTED;
        
    const BOOL b = ResetEvent(m_hStateChangeOrFlush);
    assert(b);

    if (!m_bFlush)
        return S_FALSE;  //?

    m_bFlush = false;
    
    //TODO: it's not clear whether this should also clear
    //the EOS status.
    
    //TODO: re-create stream object

    return S_OK;
}


HRESULT Inpin::NewSegment( 
    REFERENCE_TIME,
    REFERENCE_TIME,
    double)
{
    Lock lock(this);
        
    if (m_connection == 0)
        return VFW_E_NOT_CONNECTED;
        
    //TODO: we could probably tell file object to render its
    //current cluster (and start a new one)
        
    return S_OK;  //TODO
}


HRESULT Inpin::GetAllocator(IMemAllocator** p)
{
    if (p)
        *p = 0;
        
    return VFW_E_NO_ALLOCATOR;  //?
}


HRESULT Inpin::NotifyAllocator( 
    IMemAllocator*,
    BOOL)
{
    return S_OK;
}


HRESULT Inpin::Receive(IMediaSample* pSample)
{
    if (pSample == 0)
        return E_INVALIDARG;

#if 0 //def _DEBUG
    {
        __int64 start_reftime, stop_reftime;
        const HRESULT hr = p->GetTime(&start_reftime, &stop_reftime);

        odbgstream os;
        os << "flvmux::outpin::enque_video: ";
        
        if (hr == S_OK)
            os << "start=" << double(start_reftime) / 10000000
               << "; stop=" << double(stop_reftime) / 10000000;

        else if (hr == VFW_S_NO_STOP_TIME)
            os << "start=" << double(start_reftime) / 10000000;

        os << endl;
    }
#endif

    Lock lock(this);
    
#if 0     
    wodbgstream os;
    os << L"inpin[" << m_id << "]::Receive: THREAD=0x"
       << hex << GetCurrentThreadId() << dec
       << endl;
#endif

    if (m_connection == 0)
        return VFW_E_NOT_CONNECTED;
    
    if (m_pFilter->m_state == State_Stopped)
        return VFW_E_NOT_RUNNING;

    if (m_bEndOfStream)
        return VFW_E_SAMPLE_REJECTED_EOS;

    if (m_bFlush)
        return S_FALSE;
      
    const HRESULT hr = m_pStream->Receive(pSample);
    assert(hr == S_OK);  //TODO
    
    const BOOL b = SetEvent(m_hSample);  //notify other pin
    assert(b);
    
    return Wait(lock);
}


HRESULT Inpin::Wait(Lock& lock)
{
    //TODO:
    //If pStream->Wait returns true, it implies that there's
    //another stream.  However, even without another pin
    //(that is, pStream->Wait returns false), we must still
    //wait if we're paused. 
    
    //If video stream is ahead of audio, then wait for audio.
    //Audio stream will signal when new sample is delivered.
    
    //If there's no audio stream connection, then don't wait.
    
    //If audio stream is EOS, then don't wait.
    
    //While we're waiting, we must wake up because:
    //  receipt of audio sample
    //  flush
    //  transition to stopped
    //  any other transition?
    
    //We must signal receipt of this video sample, so audio
    //streaming thread will wake up.
    
    //How do we detect whether video stream is ahead of audio?
    
    //If we wait, we must release the filter lock.
    
    //If we transition to stopped, then wake up and release caller.
    
    //If we're paused, then write frame, and block caller.
    //If we transition from paused, then wake up and release caller.
    
    const HANDLE hOther = GetOtherHandle();
    
    enum { cHandles = 2 };
    HANDLE hh[cHandles] = { m_hStateChangeOrFlush, hOther };

#if 0    
    wodbgstream os;
#endif
    
    for (;;)
    {
        if (m_bFlush)
        {
#if 0        
            os << L"inpin[" << m_id << "]::wait: FLUSH; EXITING" << endl;
#endif

            return S_FALSE;
        }
            
        const FILTER_STATE state = m_pFilter->m_state;
        
        if (state == State_Stopped)
        {
#if 0        
            os << L"inpin[" << m_id << "]::wait: STATE=STOPPED; EXITING" << endl;
#endif

            return VFW_E_NOT_RUNNING;
        }

        if ((state == State_Running) && !m_pStream->Wait())
        {
#if 0        
            os << L"inpin[" 
               << m_id 
               << "]::wait: STATE=RUNNING and !WAIT; EXITING; THREAD=0x"
               << hex << GetCurrentThreadId() << dec
               << endl;
#endif

            return S_OK;
        }

#if 0        
        os << L"inpin[" << m_id << "]::wait: state=" 
           << state
           << "; WAITING; THREAD=0x" 
           << hex << GetCurrentThreadId() << dec
           << endl;
#endif
        
        assert(m_connection);
        assert(!m_bEndOfStream);
            
        lock.Release();
    
        DWORD index;
            
        const HRESULT hr = CoWaitForMultipleHandles(
                            0,  //wait flags
                            INFINITE,
                            cHandles,
                            hh,
                            &index);
                
        assert(hr == S_OK);
        
        lock.Seize(this);
    }
}


HRESULT Inpin::ReceiveMultiple(   //TODO: handle wait here, instead of in Receive?
    IMediaSample** pSamples,
    long n,    //in
    long* pm)  //out
{
    if (pm == 0)
        return E_POINTER;
        
    long& m = *pm;    //out
    m = 0;
    
    if (n <= 0)
        return S_OK;  //weird
    
    if (pSamples == 0)
        return E_INVALIDARG;
        
    Lock lock(this);
    
#if 0
    wodbgstream os;
    os << L"inpin[" << m_id << "]::ReceiveMultiple(begin): THREAD=0x"
       << hex << GetCurrentThreadId() << dec
       << endl;
#endif

    if (m_connection == 0)
        return VFW_E_NOT_CONNECTED;
    
    if (m_pFilter->m_state == State_Stopped)
        return VFW_E_WRONG_STATE;  //?

    if (m_bEndOfStream)
        return VFW_E_SAMPLE_REJECTED_EOS;

    if (m_bFlush)
        return S_FALSE;
      
    for (long i = 0; i < n; ++i)
    {
        IMediaSample* const pSample = pSamples[i];
        assert(pSample);
        
#if 0
        os << L"inpin[" << m_id << "]::ReceiveMultiple(cont'd):";
        
        __int64 st, sp;
        
        HRESULT hr = pSample->GetTime(&st, &sp);
        
        if (hr == S_OK)
            os << " st=" << st << " sp=" << sp;
        else if (SUCCEEDED(hr))
            os << " st=" << st;
        else
            os << " (NO TIME SET)";
            
        os << "; PREROLL=" 
           << ((pSample->IsPreroll() == S_OK) ? "TRUE" : "FALSE");
           
        os << endl;
#endif

        const HRESULT hr = m_pStream->Receive(pSample);
        assert(hr == S_OK);
        
        ++m;
    }

    const BOOL b = SetEvent(m_hSample);  //notify other pin
    assert(b);
    
    return Wait(lock);
}


HRESULT Inpin::ReceiveCanBlock()
{
    return S_OK;  //yes, we block caller
}



HRESULT Inpin::OnReceiveConnection(IPin*, const AM_MEDIA_TYPE&)
{
    return S_OK;
}


HRESULT Inpin::OnEndOfStream()
{
#if 0
    wodbgstream os;
    os << L"inpin[" << m_id << L"]::EndOfStream (before calling stream)" << endl;
#endif

    const int result = m_pStream->EndOfStream();
    
#if 0        
    os << L"inpin[" << m_id << L"]::EndOfStream (after calling stream); result="
       << result
       << endl;
#endif

    const BOOL b = SetEvent(m_hSample);  //notify other pin
    assert(b);

    if (result <= 0)
        return S_OK;
        
    const HRESULT hr = m_pFilter->OnEndOfStream();
    hr;
    assert(SUCCEEDED(hr));
    
    return S_OK;
}


}  //end namespace MkvMux
